// ==UserScript==
// @name         crawler
// @namespace    http://tampermonkey.net/
// @version      0.6
// @description  auxiliary tool for crawler
// @author       wushen-zhong
// @match        *://*/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=0.1
// @grant        unsafeWindow
// @grant        GM_addElement
// @grant        GM_addStyle
// @grant        GM_download
// @grant        GM_getResourceText
// @grant        GM_getResourceURL
// @grant        GM_info
// @grant        GM_log
// @grant        GM_notification
// @grant        GM_openInTab
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_setClipboard
// @grant        GM_getTab
// @grant        GM_saveTab
// @grant        GM_getTabs
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_setValues
// @grant        GM_getValues
// @grant        GM_deleteValues
// @grant        GM_addValueChangeListener
// @grant        GM_removeValueChangeListener
// @grant        GM_xmlhttpRequest
// @grant        GM_webRequest
// @grant        GM_cookie
// @grant        window.onurlchange
// @grant        window.close
// @grant        window.focus
// @downloadURL https://gitee.com/wushen_zhong/browsers-crawler/raw/develop/userscript.js
// @updateURL https://gitee.com/wushen_zhong/browsers-crawler/raw/develop/userscript_meta.js
// ==/UserScript==

(function () {
  'use strict';

  GM_registerMenuCommand('运行gitee上的脚本', () => {
    const runtime = {
      unsafeWindow,
      GM_addElement, GM_addStyle,
      GM_download,
      GM_getResourceText, GM_getResourceURL,
      GM_info, GM_log,
      GM_notification,
      GM_openInTab,
      GM_registerMenuCommand, GM_unregisterMenuCommand,
      GM_setClipboard,
      GM_getTab, GM_saveTab, GM_getTabs,
      GM_setValue, GM_getValue, GM_deleteValue,
      GM_listValues, GM_setValues, GM_getValues, GM_deleteValues,
      GM_addValueChangeListener, GM_removeValueChangeListener,
      GM_xmlhttpRequest,
      GM_webRequest,
      GM_cookie
    }
    Function(
      ...Object.keys(runtime),
      document.querySelector("#tree-content-holder > div > div.file_content.code > div > div.highlight.markdown-code-block > pre").innerText
    ).call(
      window,
      ...Object.values(runtime)
    )
  })

  if (!window.opener) return

  /**
   * https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified
   */
  class Timer extends EventTarget {
    #delay
    #startTime
    #timeoutId
    #hasElapsedTime = 0
    static #baseId = 0
    static #timeoutIdMap = new Map()
    static #intervalIdMap = new Map()
    constructor(delay) {
      super()
      this.#delay = delay
    }
    start() {
      this.#startTime = new Date().getTime()
      const handle = () => {
        this.#hasElapsedTime += new Date().getTime() - this.#startTime
        if (this.#delay < this.#hasElapsedTime)
          this.dispatchEvent(new CustomEvent('timeout'))
        else
          this.#timeoutId = setTimeout(handle, this.#delay - this.#hasElapsedTime)
      }
      handle()
    }
    reset() {
      this.#startTime = new Date().getTime()
      this.#hasElapsedTime = 0
    }
    stop() {
      clearTimeout(this.#timeoutId)
      this.#hasElapsedTime += new Date().getTime() - this.#startTime
    }
    static setTimeout(handle, delay) {
      const timer = new Timer(delay)
      timer.addEventListener('timeout', handle)
      timer.start()
      const id = Timer.#baseId++
      Timer.#timeoutIdMap.set(id, timer)
      return id
    }
    static setInterval(handle, delay) {
      const timer = new Timer(delay)
      timer.addEventListener('timeout', () => {
        handle()
        timer.reset()
        timer.start()
      })
      timer.start()
      const id = Timer.#baseId++
      Timer.#intervalIdMap.set(id, timer)
      return id
    }
    static clearTimeout(id) {
      Timer.#timeoutIdMap.get(id).stop()
      Timer.#timeoutIdMap.delete(id)
    }
    static clearInterval(id) {
      Timer.#intervalIdMap.get(id).stop()
      Timer.#intervalIdMap.delete(id)
    }
    static wait(delay) {
      const { promise, resolve } = Promise.withResolvers()
      const timer = new Timer(delay)
      timer.addEventListener('timeout', resolve)
      timer.start()
      return promise
    }
  }

  class TabSession extends EventTarget {
    static #label = 'using crawler in browser'
    #id
    constructor() { super() }
    listen(timeout = 10000) {
      if (timeout >= 0)
        var connectTimeoutId = Timer.setTimeout(() => this.dispatchEvent(new CustomEvent('timeout', { detail: new Error('timeout occurred during connection') })), timeout)
      const handle = ({ data: { label, id } }) => {
        if (label !== TabSession.#label)
          return
        if (timeout >= 0) {
          Timer.clearTimeout(connectTimeoutId)
          var timer = new Timer(timeout)
          timer.addEventListener('timeout', () => this.dispatchEvent(new CustomEvent('timeout', { detail: new Error('timeout occurred while keeping the connection') })))
          timer.start()
        }
        window.removeEventListener('message', handle)
        this.#id = id
        window.addEventListener('message', ({ data: { label, id, heartbeat, data } }) => {
          if (label != TabSession.#label || id !== this.#id)
            return
          if (timeout >= 0)
            timer.reset()
          if (heartbeat) {
            window.opener.postMessage({ label: TabSession.#label, id: this.#id, heartbeat: true }, '*')
            return
          }
          this.dispatchEvent(new CustomEvent('message', { detail: data }))
        })
        window.opener.postMessage({ label: TabSession.#label, id: this.#id, heartbeat: true }, '*')
        this.dispatchEvent(new CustomEvent('connect'))
      }
      window.addEventListener('message', handle)
    }
    close() { }
    send(data) {
      window.opener.postMessage({ label: TabSession.#label, id: this.#id, data }, '*')
    }
  }

  const session = new TabSession()

  let idCache = 0
  function log(data, key = idCache++) {
    session.send({ type: 'log', key, data })
    return key
  }

  const runtime = {}
  runtime.scrollToEnd = async () => {
    let tryTimes = 0
    let lastScrollY = 0
    while (tryTimes < 10) {
      window.scrollBy(0, window.innerHeight)
      if (lastScrollY == window.scrollY)
        tryTimes++
      else
        tryTimes = 0
      lastScrollY = window.scrollY
      await Timer.wait(200)
    }
  }
  runtime.wait = Timer.wait
  runtime.awaitAll = async (obj) => {
    for (const key in obj)
      obj[key] = await obj[key]
    return obj
  }
  runtime.gather = async (handle, f = [undefined], time = 200) => {
    const lid = log({ handle: `gather(${handle.toString()})`, progress: 0 })
    const isFunc = typeof f == 'function'
    let res
    while (true) {
      res = handle()
      if ((isFunc && f(res)) || !f.includes(res))
        break
      await runtime.wait(time)
    }
    log({ handle: `gather(${handle.toString()})`, progress: 100 }, lid)
    return res
  }
  runtime.get = (url, rollback = false) => {
    const lid = log({ handle: `get('${url}')`, progress: 0 })
    let { promise, resolve, reject } = Promise.withResolvers()
    const xhr = new XMLHttpRequest()
    xhr.open("GET", url)
    xhr.addEventListener('progress', ev => log({ handle: `get('${url}')`, progress: Math.round(ev.loaded / ev.total * 100) }, lid))
    xhr.addEventListener('readystatechange', () => {
      if (xhr.readyState === 4)
        resolve(xhr.response)
    })
    xhr.addEventListener('error', () => {
      if (rollback)
        download(url)
      else
        reject(xhr.status + xhr.statusText)
    })
    xhr.responseType = 'blob'
    xhr.send()
    return promise
  }
  runtime.download = async (url) => {
    const a = document.createElement('a')
    a.href = url
    document.body.append(a)
    a.click()
    await runtime.wait(16000)
    a.remove()
    const lid = log({ handle: `download(${url})`, progress: 0 })
    log({ handle: `download(${url})`, progress: 100 }, lid)
  }

  session.addEventListener('timeout', ({ detail }) => console.warn(detail))
  session.addEventListener('message', async ({ detail: { key, handle } }) => {
    const data = { type: 'result', key }
    try {
      data.data = await Function(...Object.keys(runtime), `return (${handle})()`).apply(unsafeWindow, Object.values(runtime))
    } catch (err) {
      data.error = err
    }
    session.send(data)
  })
  session.listen()

})();